/*
        Copyright 2018 Gaurav Kumar

        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
*/

package com.gauravk.audiovisualizer.visualizer;

import com.gauravk.audiovisualizer.base.BaseVisualizer;
import com.gauravk.audiovisualizer.model.AnimSpeed;
import com.gauravk.audiovisualizer.model.PaintStyle;
import com.gauravk.audiovisualizer.utils.BezierSpline;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Path;
import ohos.agp.utils.Point;
import ohos.app.Context;
import ohos.media.audio.AudioManager;
import ohos.media.audio.AudioRemoteException;

/**
 * Custom view to create blob visualizer
 * <p>
 * Created by gk
 */
public class BlobVisualizer extends BaseVisualizer implements Component.DrawTask {
    private static final int BLOB_MAX_POINTS = 60;
    private static final int BLOB_MIN_POINTS = 3;

    private Path mBlobPath;
    private int mRadius;

    private int nPoints;

    private Point[] mBezierPoints;
    private BezierSpline mBezierSpline;
    private boolean isFirstPlay = true;
    private float mAngleOffset;
    private float mChangeFactor;

    /**
     * 使用java代码实例化调用
     *
     * @param context
     */
    public BlobVisualizer(Context context) {
        super(context);
    }

    /**
     * 使用xml布局引用调用
     *
     * @param context
     * @param attrSet
     */
    public BlobVisualizer(Context context, AttrSet attrSet) {
        super(context, attrSet);
    }

    /**
     * 使用xml并设置style调用
     *
     * @param context
     * @param attrSet
     * @param styleName
     */
    public BlobVisualizer(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
    }

    @Override
    protected void init() {
        addDrawTask(this::onDraw);
        mRadius = -1;
        nPoints = (int) (mDensity * BLOB_MAX_POINTS);
        if (nPoints < BLOB_MIN_POINTS) {
            nPoints = BLOB_MIN_POINTS;
        }

        mAngleOffset = (360.0f / nPoints);

        updateChangeFactor(mAnimSpeed, false);

        mBlobPath = new Path();

        // initialize mBezierPoints, 2 extra for the smoothing first and last point
        mBezierPoints = new Point[nPoints + 2];
        for (int i = 0; i < mBezierPoints.length; i++) {
            mBezierPoints[i] = new Point();
        }

        mBezierSpline = new BezierSpline(mBezierPoints.length);
    }

    @Override
    public void setAnimationSpeed(AnimSpeed animSpeed) {
        super.setAnimationSpeed(animSpeed);
        updateChangeFactor(animSpeed, true);
    }

    private void updateChangeFactor(AnimSpeed animSpeed, boolean useHeight) {
        int height = 1;
        if (useHeight) {
            height = getHeight() > 0 ? getHeight() : 1000;
        }

        if (animSpeed == AnimSpeed.SLOW) {
            mChangeFactor = height * 0.003f;
        } else if (animSpeed == AnimSpeed.MEDIUM) {
            mChangeFactor = height * 0.006f;
        } else {
            mChangeFactor = height * 0.01f;
        }
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        double angle = 0;
        angle = onDrawInit(angle);

        // create the path and draw
        if (isVisualizationEnabled && mRawAudioBytes != null) {
            if (mRawAudioBytes.length == 0) {
                return;
            }

            mBlobPath.rewind();

            // find the destination bezier point for a batch
            onDrawValuation(component, angle);

            onDrawModification();

            // add an extra line to center cover the gap generated by last cubicTo
            if (mPaintStyle == PaintStyle.FILL) {
                mBlobPath.lineTo(getWidth() / 2, getHeight() / 2);
            }
            canvas.drawPath(mBlobPath, mPaint);
        }
        super.addDrawTask(this::onDraw);
    }

    private void onDrawModification() {
        // set the first and last point as first

        mBezierPoints[nPoints].modify(mBezierPoints[0].getPointX(), mBezierPoints[0].getPointY());
        mBezierPoints[nPoints + 1].modify(mBezierPoints[0].getPointX(), mBezierPoints[0].getPointY());

        // update the control points
        mBezierSpline.updateCurveControlPoints(mBezierPoints);
        Point[] firstCP = mBezierSpline.getFirstControlPoints();
        Point[] secondCP = mBezierSpline.getSecondControlPoints();

        // create the path
        mBlobPath.moveTo(mBezierPoints[0].getPointX(), mBezierPoints[0].getPointY());
        for (int i = 0; i < firstCP.length; i++) {
            mBlobPath.cubicTo(firstCP[i].getPointX(), firstCP[i].getPointY(),
                    secondCP[i].getPointX(), secondCP[i].getPointY(),
                    mBezierPoints[i + 1].getPointX(), mBezierPoints[i + 1].getPointY());
        }
    }

    private void onDrawValuation(Component component, double angle) {
        for (int i = 0; i < nPoints; i++, angle += mAngleOffset) {

            // 1=1 X=34  i=2 X=68
            int x = (int) Math.ceil((i + 1) * (mRawAudioBytes.length / nPoints));
            int t = 0;
            if (x < 1024) {
                t = ((byte) (-Math.abs(mRawAudioBytes[x]) + 128)) * (component.getHeight() / 4) / 128;
            }

            float posX = (float) (getWidth() / 2
                    + (mRadius + t)
                    * Math.cos(Math.toRadians(angle)));

            float posY = (float) (getHeight() / 2
                    + (mRadius + t)
                    * Math.sin(Math.toRadians(angle)));

            // calculate the new x based on change
            if (posX - mBezierPoints[i].getPointX() > 0) {
                mBezierPoints[i].modify(mBezierPoints[i].getPointX() + mChangeFactor, mBezierPoints[i].getPointY());
            } else {
                mBezierPoints[i].modify(mBezierPoints[i].getPointX() - mChangeFactor, mBezierPoints[i].getPointY());
            }

            // calculate the new y based on change
            if (posY - mBezierPoints[i].getPointY() > 0) {
                mBezierPoints[i].modify(mBezierPoints[i].getPointX(), mBezierPoints[i].getPointY() + mChangeFactor);
            } else {
                mBezierPoints[i].modify(mBezierPoints[i].getPointX(), mBezierPoints[i].getPointY() - mChangeFactor);
            }
        }
    }

    private double onDrawInit(double angle) {
        // first time initialization
        if (mRadius == -1) {
            mRadius = getHeight() < getWidth() ? getHeight() : getWidth();

            // 382
            mRadius = (int) (mRadius * 0.65 / 2);

            // 10.8
            mChangeFactor = getHeight() * mChangeFactor;

            // initialize bezier points
            for (int i = 0; i < nPoints; i++, angle += mAngleOffset) {
                float posX = (float) (getWidth() / 2
                        + (mRadius)
                        * Math.cos(Math.toRadians(angle)));

                float posY = (float) (getHeight() / 2
                        + (mRadius)
                        * Math.sin(Math.toRadians(angle)));

                mBezierPoints[i].modify(posX, posY);
            }
        }
        return angle;
    }
}